package com.rogers.rmcdouga.skirmishgadget.client;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;

import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.ImageElement;
import com.google.gwt.dom.client.SpanElement;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.LoadEvent;
import com.google.gwt.event.dom.client.MouseDownEvent;
import com.google.gwt.event.dom.client.MouseMoveEvent;
import com.google.gwt.event.dom.client.MouseMoveHandler;
import com.google.gwt.event.dom.client.MouseUpEvent;
import com.google.gwt.event.dom.client.MouseUpHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.gen2.logging.shared.Log;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.uibinder.client.UiHandler;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.PushButton;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwt.widgetideas.graphics.client.GWTCanvas;
import com.rogers.rmcdouga.skirmishgadget.client.java.awt.Dimension;
import com.rogers.rmcdouga.skirmishgadget.client.java.awt.Point;
import com.rogers.rmcdouga.skirmishgadget.client.java.awt.geom.Point2D;
import com.rogers.rmcdouga.skirmishgadget.client.java.util.UUID;

public class BattleMap extends Composite
{
	// public members
	static final Point ORIGIN = new Point(0,0);
	static final float[] zoomLevelPresets = {0.06f, 0.125f, 0.25f, 0.50f, 0.75f, 1f, 1.5f, 2f, 3f, 4f, 5f, 6f};
	static final String invisibleImageContainerName = UUID.randomUUID().toString();
	
	// private members
	private boolean initialized = false;
	
	private List<Token> tokenList;
	private float zoomLevel;		// 1 = 100%, .5 = 50%, 2 = 200%, etc.
	private int squareSizeInFeet = 5;
	private Dimension squareSize;
	private float panX;				// value must be >=0 and <= 1
	private float panY;				// value must be >=0 and <= 1

	// Constructors
	public BattleMap() {
		super();
		initWidget(uiBinder.createAndBindUi(this));
		
		this.tokenList = new ArrayList<Token>();
		this.invisibleImageContainer.setId(invisibleImageContainerName);
		setZoomLevel(1f);
	}

//	public BattleMap(String mapUrl, int width, int height) {
//		this(mapUrl, new Dimension(width, height));
//	}
//	
//	public BattleMap(String mapUrl, Dimension size) {
//		this();
//
//		this.map = new Image();
//
//		// Load and Display the image
//		final GWTCanvas myCanvas = canvas;	// anonymous classes prefer final variables
//		this.map.addLoadHandler(new LoadHandler()
//			{
//				public void onLoad(LoadEvent event) {
//					Log.info("Inside BattleMap Image Load event handler");
//					
//					panX = panY = 0;
//					// Calculate a zoom level that will fit the page
//					zoomToFit(ZoomToFit.BOTH);
//					canvas.setHeight(Integer.toString(map.getHeight()) + " px");
//					canvas.setWidth(Integer.toString(map.getWidth()) + " px");
//					initialized = true;
//					draw();
//				}
//
//			}
//			);
//
//		setUrl(mapUrl);
//	}

	// Beginning of UI Code
	private SetMapDialog dlg_SetMapDialog;
	private AddTokenDialog dlg_AddTokenDialog;
	private GridDialog dlg_GridDialog;
	private AboutDialog dlg_AboutDialog;


	private static BattleMapUiBinder uiBinder = GWT.create(BattleMapUiBinder.class);

	interface BattleMapUiBinder extends UiBinder<Widget, BattleMap>
	{
	}

	@UiField
	SpanElement invisibleImageContainer;

	@UiField
	Image map;

	@UiHandler("map")
	public void map_OnLoad(LoadEvent event)
	{
		Log.info("Inside BattleMap Image Load event handler");

		// The load only happens once for each image, so we re-invoke setUrl
		// because the RootPanel wasn't in a good state when it was called the first time.
		this.setUrl(this.map.getUrl());
		zoomToFit(ZoomToFit.BOTH);
	}

	@UiField
	BattleMapCanvas canvas;
	
	@UiHandler("canvas")
	public void canvas_OnMouseDown(MouseDownEvent event) {
		Log.info("Inside BattleMap Mouse Down event handler - getX()=" + event.getX() + ", getY()=" + event.getY()
				+ ", getClientX()=" + event.getClientX() + ", getClientY()=" + event.getClientY()
				+ ".");
		Point mapPoint = convertScreenPointToMapPoint(new Point(event.getX(), event.getY()));
		Log.info("Checking if screenPoint " + mapPoint.x + "," + mapPoint.y + " is within Token's borders.");
		
		Token selectedToken = checkForToken(mapPoint);
		if (selectedToken != null)
		{
			selectedToken.setSelected("Mover & Shaker");
			Point tokenScreenLocation = convertMapPointToScreenPoint(selectedToken.getLocation());
			int screenOffsetX = event.getX() - tokenScreenLocation.x - Math.round(getCurrentOriginX());
			int screenOffsetY = event.getY() - tokenScreenLocation.y - Math.round(getCurrentOriginY());
			
			class TokenMover
			{
				Token mySelectedToken;
				private HandlerRegistration mouseMoveHandler;
				private HandlerRegistration mouseUpHandler;
				int screenOffsetX, screenOffsetY;
				

				public TokenMover(Token mySelectedToken, int offsetX, int offsetY) {
					super();
					this.mySelectedToken = mySelectedToken;
					this.screenOffsetX = offsetX;
					this.screenOffsetY = offsetY;
				}
				
				public void createHandlers()
				{
					mouseMoveHandler = canvas.addMouseMoveHandler(new MouseMoveHandler()
					{
						public void onMouseMove(MouseMoveEvent event) {
							Log.info("Inside TokenMover Mouse Move event handler - getX()=" + event.getX() + ", getY()=" + event.getY()
									+ ", offsetX()=" + screenOffsetX + ", offsetY=" + screenOffsetY
									+ ".");
							mySelectedToken.move(convertScreenPointToMapPoint(new Point(event.getX() - screenOffsetX, event.getY() - screenOffsetY)));
						}
					}
					);
					
					mouseUpHandler = canvas.addMouseUpHandler(new MouseUpHandler()
					{
						public void onMouseUp(MouseUpEvent event) {
							Log.info("Inside TokenMover Mouse Up event handler - getX()=" + event.getX() + ", getY()=" + event.getY()
									+ ", getClientX()=" + event.getClientX() + ", getClientY()=" + event.getClientY()
									+ ".");
							mouseMoveHandler.removeHandler();
							mouseUpHandler.removeHandler();
							mySelectedToken.setDeSelected();
							redraw();
						}
					}
					);
				}
			}
			final TokenMover tokenMover = new TokenMover(selectedToken, screenOffsetX, screenOffsetY);
			tokenMover.createHandlers();
		}
	}

	@UiHandler("canvas")
	public void canvas_OnMouseMove(MouseMoveEvent event) {
		Point mapPoint = convertScreenPointToMapPoint(new Point(event.getX(), event.getY()));
		Token selectedToken = checkForToken(mapPoint);
//		Log.info("Inside BattleMap Mouse Move event handler - getX()=" + event.getX() + ", getY()=" + event.getY()
//				+ ", getClientX()=" + event.getClientX() + ", getClientY()=" + event.getClientY()
//				+ ".");

		if (selectedToken != null)
		{
			Log.info("MouseOver token '" + selectedToken.getName() + "'.");
			canvas.setTitle(selectedToken.getName());
		}
		else
		{
			canvas.setTitle("");			
		}
	}

	private Token checkForToken(Point mapPoint)
	{
		// find the *last* token in the list that is under the cursor (i.e. the token on top).
		Token selectedToken = null;
		for(Token t: tokenList)
		{
			if (t.isPointContained(mapPoint))
			{
				Log.info("Found a match!!!");
				selectedToken = t;
			}
		}
		return selectedToken;
	}

	@UiField
	PushButton mapButton;

	@UiHandler("mapButton")
	void mapButton_OnClick(ClickEvent e)
	{
	    if (dlg_SetMapDialog == null) 
	    	dlg_SetMapDialog = new SetMapDialog(map.getUrl());
	    dlg_SetMapDialog.setOriginalMapWidth(canvas.getCoordWidth());   
	    dlg_SetMapDialog.setOriginalMapHeight(canvas.getCoordHeight());
	    dlg_SetMapDialog.setOkAction(new SetMapDialog.DialogResult() {
		
			@Override
			public void action(SetMapDialog dlg)
			{
				setUrlAndResize(dlg.getMapURL(), dlg.getMapWidth(), dlg.getMapHeight());
				redraw();
			}
		});
	    dlg_SetMapDialog.show();
	    dlg_SetMapDialog.center();
		redraw();
	}

	@UiField
	PushButton addTokenButton;
	
	@UiHandler("addTokenButton")
	void addTokenButton_OnClick(ClickEvent event)
	{
	    if (dlg_AddTokenDialog == null)
	    	dlg_AddTokenDialog = new AddTokenDialog();
	    final BattleMap battle_map = this;
	    dlg_AddTokenDialog.setOkAction(new AddTokenDialog.DialogResult() {
	    	
			@Override
			public void action(AddTokenDialog dlg)
			{
				// Add the token in the top left hand corner of the current view.
				Token.CreateToken(new Point(Math.round(battle_map.getCurrentOriginX() * -1), Math.round(battle_map.getCurrentOriginY() * -1)), dlg.getTokenURL(), battle_map, dlg.getTokenName());
				redraw();
			}
		});
	    dlg_AddTokenDialog.setExistingTokens(getTokenListAsHashMap());
	    
	    dlg_AddTokenDialog.show();
	    dlg_AddTokenDialog.center();
		redraw();
	}

	@UiField
	PushButton delTokenButton;
	
	@UiHandler("delTokenButton")
	void delTokenButton_OnClick(ClickEvent event)
	{
		// TODO: Add code to prompt for Token to delete
		redraw();
	}

	@UiField
	PushButton setGridButton;
	
	@UiHandler("setGridButton")
	void setGridButton_OnClick(ClickEvent event)
	{
	    if (dlg_GridDialog == null)
	    	dlg_GridDialog = new GridDialog();
	    dlg_GridDialog.setScreenGridSize(getSquareSize(ORIGIN).width);	// For now, just use the width
	    dlg_GridDialog.setMapGridSize(getSquareSizeInFeet());
	    dlg_GridDialog.setOkAction(new GridDialog.DialogResult() {
	    	
			@Override
			public void action(GridDialog dlg)
			{
				// set the appropriate values
				int square_size = dlg.getScreenGridSize();
				setSquareSize(new Dimension(square_size, square_size));
				setSquareSizeInFeet(dlg.getMapGridSize());
				redraw();
			}
		});

	    dlg_GridDialog.show();
	    dlg_GridDialog.center();
		redraw();
	}

	@UiField
	PushButton zoomInButton;
	
	@UiHandler("zoomInButton")
	void zoomInButton_OnClick(ClickEvent event)
	{
		zoomInToNextPreset();
		redraw();
	}

	@UiField
	PushButton zoomOutButton;
	
	@UiHandler("zoomOutButton")
	void zoomOutButton_OnClick(ClickEvent event)
	{
		zoomOutToNextPreset();
		redraw();
	}

	@UiField
	PushButton zoomToFitButton;
	
	@UiHandler("zoomToFitButton")
	void zoomToFitButton_OnClick(ClickEvent event)
	{
		zoomToFit(ZoomToFit.BOTH);
		redraw();
	}

	@UiField
	PushButton panUpButton;
	
	@UiHandler("panUpButton")
	void panUpButton_OnClick(ClickEvent event)
	{
		pan(PanDirection.UP);
		redraw();
	}

	@UiField
	PushButton panRightButton;
	
	@UiHandler("panRightButton")
	void panRightButton_OnClick(ClickEvent event)
	{
		pan(PanDirection.RIGHT);
		redraw();
	}
	@UiField
	PushButton panDownButton;
	
	@UiHandler("panDownButton")
	void panDownButton_OnClick(ClickEvent event)
	{
		pan(PanDirection.DOWN);
		redraw();
	}
	@UiField
	PushButton panLeftButton;
	
	@UiHandler("panLeftButton")
	void panLeftButton_OnClick(ClickEvent event)
	{
		pan(PanDirection.LEFT);
		redraw();
	}

	@UiField
	PushButton aboutButton;
	
	@UiHandler("aboutButton")
	void aboutButton_OnClick(ClickEvent event)
	{
	    if (dlg_AboutDialog == null)
	    	dlg_AboutDialog = new AboutDialog();
	    dlg_AboutDialog.show();
	    dlg_AboutDialog.center();
	}

	// End of UiHandlers
	public GWTCanvas getHtmlCanvas() {
		return canvas;
	}
	private void setHtmlCanvas(BattleMapCanvas canvas) {
		this.canvas = canvas;
	}

	public List<Token> getTokenList() {
		return tokenList;
	}
	protected void setTokenList(List<Token> tokenList) {
		this.tokenList = tokenList;
	}

	public void setSquareSize(Dimension d)
	{
		this.squareSize = d;
	}
	public Dimension getSquareSize(Point p)
	{
		// For now, this returns the same value everywhere however later it may return slightly different
		// values depending on where in the map the Point p is.
		return squareSize;
	}
	
	public void setFiveFootSquare(Dimension squareSize)
	{
		this.squareSize = squareSize;
	}

	public float getZoomLevel()
	{
		return zoomLevel;
	}
	public void setZoomLevel(float zoomLevel)
	{
		if (zoomLevel < 0)
			throw new IllegalArgumentException("Illegal Zoom Level value: '" + zoomLevel + "'. Zoom level cannot be < 0.");
		this.zoomLevel = zoomLevel;
	}
	public enum ZoomToFit { HEIGHT, WIDTH, BOTH };
	public void zoomToFit(ZoomToFit ztf)
	{
		float zoom_width = Integer.valueOf(canvas.getCoordWidth()).floatValue() / Integer.valueOf(map.getWidth()).floatValue();
		float zoom_height = Integer.valueOf(canvas.getCoordHeight()).floatValue() / Integer.valueOf(map.getHeight()).floatValue();
		Log.info("Inside BattleMap setZoomLevelToFitImage: myCanvas.getCoordWidth()=" + canvas.getCoordWidth() + ", myCanvas.getCoordHeight()=" + canvas.getCoordHeight() + ".");
		Log.info("Inside BattleMap setZoomLevelToFitImage: map.getWidth()=" + map.getWidth() + ", map.getHeight()=" + map.getHeight() + ".");
		Log.info("Inside BattleMap setZoomLevelToFitImage: zoom_width=" + zoom_width + ", zoom_height=" + zoom_height + ".");
		float zoom_amt;
		switch(ztf)
		{
			case HEIGHT:
				setPanY(0);
				zoom_amt = zoom_height;
				break;
			case WIDTH:
				setPanX(0);
				zoom_amt = zoom_width;
				break;
			case BOTH:
				setPanX(0);
				setPanY(0);
				zoom_amt = Math.min(zoom_width, zoom_height);
				break;
			default:
				throw new IllegalArgumentException("Unknown ZoomToFit enum value");
		}
		setZoomLevel(zoom_amt);
	}
	public void zoomInToNextPreset()
	{
		int location = Arrays.binarySearch(zoomLevelPresets, getZoomLevel());

		// If binary search fails to find the key, it returns (-(insertion point) -1).
		if (location < 0)
			location = Math.abs(location)-2;		// if the key was not found, select the location just below the insertion point

		// Now that we've got a location in mind, set it, but make sure we don't exceed the length of the array
		setZoomLevel(zoomLevelPresets[Math.min(location + 1, zoomLevelPresets.length - 1 )]);
	}
	public void zoomOutToNextPreset()
	{
		int location = Arrays.binarySearch(zoomLevelPresets, getZoomLevel());

		// If binary search fails to find the key, it returns (-(insertion point) -1).
		if (location < 0)
			location = Math.abs(location)-1;		// if the key was not found, select the location just above the insertion point

		// Now that we've got a location in mind, set it, but make sure we don't exceed the length of the array
		setZoomLevel(zoomLevelPresets[Math.max(location - 1, 0)]);
	}

	public float getPanX()
	{
		return panX;
	}
	public void setPanX(float panX)
	{
		Log.info("Inside setPanX: panX='" + panX + "'.");
		if (panX < 0 || panX > 1)
			throw new IllegalArgumentException("Illegal PanX value: '" + panX + "'. PanX must be 0-1 inclusive.");
		this.panX = panX;
	}
	public float getPanY()
	{
		return panY;
	}
	public void setPanY(float panY)
	{
		Log.info("Inside setPanY: panY='" + panY + "'.");
		if (panY < 0 || panY > 1)
			throw new IllegalArgumentException("Illegal PanY value: '" + panY + "'. PanY must be 0-1 inclusive.");
		this.panY = panY;
	}

	public enum PanDirection { LEFT, RIGHT, UP, DOWN };
	public void pan(PanDirection direction)
	{
		float map_zoomed_width = map.getWidth() * zoomLevel;
		float map_zoomed_height = map.getHeight() * zoomLevel;

		// calc pan value to scroll half a screen's width
		float pan_amt_x = (getHtmlCanvas().getCoordWidth()/2)/map_zoomed_width;
		float pan_amt_y = (getHtmlCanvas().getCoordHeight()/2)/map_zoomed_height;

		
//		Log.info("Inside pan: pan_amt='" + pan_amt + "'.");
		
		switch(direction)
		{
			case LEFT:
				float min_ratio_x = 0;
				setPanX(Math.max(min_ratio_x, getPanX()-pan_amt_x));
				break;
			case RIGHT:
				float max_ratio_x = Math.max(0, (map_zoomed_width - getHtmlCanvas().getCoordWidth())/ map_zoomed_width);
//				Log.info("Inside pan: map_zoom_size='" + map_zoomed_width + /* "', left_over_bits='" + left_over_bits + */ "', left_over_ratio='"+ max_ratio_x + "'.");
				setPanX(Math.min(max_ratio_x, getPanX()+pan_amt_x));
				break;
			case UP:
				float min_ratio_y = 0;
				setPanY(Math.max(min_ratio_y, getPanY()-pan_amt_y));
				break;
			case DOWN:
				float max_ratio_y = Math.max(0, (map_zoomed_height - getHtmlCanvas().getCoordHeight())/ map_zoomed_height);
				setPanY(Math.min(max_ratio_y, getPanY()+pan_amt_y));
				break;
			default:
				throw new IllegalArgumentException("Illegal PanDirection value: '" + direction.name() + ".value='" + direction.toString() + "'.");
		}
	}
	public void setUrl(String mapUrl)
	{

		panX = panY = 0;				// Reset the position to the top left
		this.map.setUrl(mapUrl);		// Set the image URL.
		// Do to a bug in GWT (http://code.google.com/p/google-web-toolkit/issues/detail?id=863), images are not loaded
		// until they are attached to the DOM, so we need to add the image into an invisible container/
//		Log.info("invisibleImageContainerName='" + invisibleImageContainerName + "'.");
		Element invisibleImageElement = Document.get().getElementById(invisibleImageContainerName);
		if (invisibleImageElement != null)
		{
			Log.info("Invisible image Element found.");
			invisibleImageElement.appendChild(this.map.getElement());
			redraw();
		}
		else
			Log.info("Invisible image Element not found.");
	}

	public void setUrlAndResize(String mapUrl, Dimension size)
	{
		canvas.resize((int)Math.round(size.getWidth()), (int)Math.round(size.getHeight()));
		setUrl(mapUrl);
	}

	public void setUrlAndResize(String mapUrl, int width, int height) {
		setUrlAndResize(mapUrl, new Dimension(width, height));
	}
	public void setUrlAndResize(String mapUrl) {
		setUrl(mapUrl);
		if ( map.getWidth() == 0 && map.getHeight() == 0)
		{
			// Image isn't loaded yet
			Log.info("Image isn't loaded yet.");
		}
		else
			canvas.resize(map.getWidth(), map.getHeight());
	}


	public void draw()
	{
		Log.info("Inside BattleMap draw routine: zoom=" + getZoomLevel() + ".");
		// draw the map onto the canvas
		getHtmlCanvas().saveContext();
		getHtmlCanvas().scale(getZoomLevel(), getZoomLevel());
//		getHtmlCanvas().translate(panX * map.getWidth() * getZoomLevel() / getHtmlCanvas().getCoordWidth(), panY * map.getHeight() * getZoomLevel() / getHtmlCanvas().getCoordHeight());
		getHtmlCanvas().translate(getCurrentOriginX(), getCurrentOriginY());
		getHtmlCanvas().drawImage(ImageElement.as(map.getElement()), 0, 0, map.getWidth(), map.getHeight(), 0, 0, map.getWidth(), map.getHeight());
		getHtmlCanvas().restoreContext();
	}
	public float getCurrentOriginY()
	{
		return panY * map.getHeight() * -1;
	}
	public float getCurrentOriginX()
	{
		return panX * map.getWidth() * -1;
	}
	public Point2D getCurrentOrigin2D()
	{
		return new Point2D.Float(getCurrentOriginX(), getCurrentOriginY());
	}
	/**
	 * getCurrentOrigin() returns the location of the map's 0,0 origin relative to the viewport's top left corner. 
	 * @return
	 */
	public Point getCurrentOrigin()
	{
		return new Point(Math.round(getCurrentOriginX()), Math.round(getCurrentOriginY()));
	}
	public Point convertScreenPointToMapPoint(Point p)
	{
		Point2D currentOrigin = getCurrentOrigin2D();
		return new Point(Math.round((p.x / getZoomLevel()) - Double.valueOf(currentOrigin.getX()).floatValue()), Math.round((p.y / getZoomLevel()) - Double.valueOf(currentOrigin.getY()).floatValue()));
	}
	public Dimension convertScreenDimensionToMapDimension(Point p)
	{
		return new Dimension(Math.round(p.x / getZoomLevel()), Math.round(p.y / getZoomLevel()));
	}
	public Point convertMapPointToScreenPoint(Point p)
	{
		Point2D currentOrigin = getCurrentOrigin2D();
		return new Point(Math.round(Double.valueOf(currentOrigin.getX()).floatValue() + p.x * getZoomLevel()), Math.round(Double.valueOf(currentOrigin.getY()).floatValue() + p.y * getZoomLevel()));
	}
	public Dimension convertMapDimensionToScreenDimension(Point p)
	{
		return new Dimension(Math.round(p.x * getZoomLevel()), Math.round(p.y * getZoomLevel()));
	}
	

	public void redraw()
	{
		getHtmlCanvas().clear();
		draw();
		for (Token t: tokenList)
			t.draw();
	}
	
	public void refresh(Point location, Dimension size)
	{
		// TODO: This routine must be altered to do partial refresh.
		Log.info("Inside BattleMap refresh routine: zoom=" + getZoomLevel() + ".");
		// draw the map onto the canvas
		getHtmlCanvas().saveContext();
		getHtmlCanvas().scale(getZoomLevel(), getZoomLevel());
		getHtmlCanvas().drawImage(ImageElement.as(map.getElement()), 0, 0, map.getWidth(), map.getHeight(), 0, 0, map.getWidth(), map.getHeight());
		getHtmlCanvas().restoreContext();
	}

	private HashMap<String, Token> getTokenListAsHashMap() {
		HashMap<String, Token> tokenMap = new HashMap<String, Token>();
		for (Token t: tokenList)
			tokenMap.put(t.getName(), t);
		return tokenMap;
	}

	public int getSquareSizeInFeet()
	{
		return squareSizeInFeet;
	}

	public void setSquareSizeInFeet(int squareSizeInFeet)
	{
		this.squareSizeInFeet = squareSizeInFeet;
	}
}
